Most classes should not directly handle resources, but instead, use member variables of a type that wraps individual resources and do resource
handling for them:
- For memory, it can be
std::unique_ptr
, std::shared_ptr
, std::vector
…
- For files, it can be
std::ofstream
, std::ifstream
…
- …
Classes that avoid directly handling resources don’t need to define any of the special member functions required to properly handle resources:
destructor, copy constructor, move constructor, copy assignment operator, and move assignment operator. That’s because the versions of those functions
provided by the compiler do the right thing automatically, which is especially useful because writing these functions correctly is typically tricky
and error-prone.
Omitting all of these functions from a class is known as the Rule of Zero because no special function should be defined. This rule should apply to
the vast majority of classes.
// Compliant: vector and unique_ptr handle the resources for us
// we don't need to declare any special member function
class RuleOfZero {
public:
void useResource();
void addValue(Value const& value);
Value getValueAtIndex(int index);
private:
std::unique_ptr<Resource> resource = std::make_unique<Resource>();
std::vector<Value> values;
};
The remaining classes that cannot use the Rule of Zero should be dedicated to managing a specific kind of resource and should follow a few logical
rules:
- Copy operations only make sense when the corresponding move operations are available. That is because move operations are optimized copy
operations allowed to steal resources from the source (the source is an r-value). At worst, copying is a valid implementation of move operations.
- Copy and move assignment operators only make sense when the corresponding constructor is available.
- If you need to customize one of the special member functions, it means that you directly handle resources and the other special member
functions probably have a role to play in this resource management. Using
= default
only performs memberwise operations.
From these rules, we can describe three categories among which all classes should fall into.
Copyable classes
Like most simple classes, these classes can be copied and moved.
Special member functions for copyable classes
- Copy construction and move construction should both be available.
- Either copy assignment and move assignment are both available, or neither is.
Providing custom special member functions for copyable classes
If at least one special function needs to be customized, then:
- You need to provide a custom destructor and a custom copy constructor.
- The copy assignment needs to be either deleted or customized.
- If you can optimize the move construction, compared to the copy, you should provide a custom move constructor. Otherwise, you should just omit
the move constructor.
- If the copy assignment is deleted, you need to delete the move assignment.
- If the copy assignment is customized, you need to provide a move assignment if you can optimize the move operation, compared to the copy.
Otherwise, you should just omit the move assignment operator.
Examples of copyable classes
// Compliant, no copy assignment operator. Move construction will call the copy constructor.
class CountedCopyable {
inline static int count = 0;
public:
CountedCopyable() { count++; }
~CountedCopyable() { count--; }
CountedCopyable(CountedCopyable const&) {count ++;}
CountedCopyable& operator=(CountedCopyable const&) = delete;
};
// Compliant, all members are declared
class VerboseCopyable {
public:
VerboseCopyable() { std::cout << "Constructor called\n"; }
~VerboseCopyable() { std::cout << "Destructor called\n"; }
VerboseCopyable(VerboseCopyable const&) { std::cout << "Copy constructor called\n"; }
VerboseCopyable& operator=(VerboseCopyable const&) { std::cout << "Copy assignment operator called\n"; }
VerboseCopyable(VerboseCopyable &&) { std::cout << "Move constructor called\n"; }
VerboseCopyable& operator=(VerboseCopyable &&) { std::cout << "Move assignment operator called\n"; }
};
Move-only classes
These are classes that cannot be copied but can be moved. For example, a class handling a resource that cannot be shared
(std::ofstream
manages an open file handle) or a class whose objects can be very costly to create.
Special member functions for move-only classes
- Move construction is available.
- Copy construction and copy assignment are not available.
- Move assignment may be available or not.
Providing custom special member functions for move-only classes
- You need to provide a custom destructor and a custom move constructor.
- The move assignment should be either deleted or customized.
Examples of move-only classes
// Compliant, the move assignment operator is implicitly deleted.
class MoveOnlyResourceHandler {
Resource resource;
public:
MoveOnlyResourceHandler() { resource.open(); }
~MoveOnlyResourceHandler() { resource.close(); }
MoveOnlyResourceHandler(MoveOnlyResourceHandler const& other) { std::swap(other.resource, resource); }
};
Unmovable classes
These are classes that cannot be copied, nor moved. They cannot escape their current scope.
Special member functions for unmovable classes
All copy and move operations are deleted.
Examples of unmovable classes
// Compliant, deleting the move assignment operator implicitly deletes all implicit special member functions
class UnmovableResource {
Resource resource;
public:
UnmovableResource() { resource.open(); }
~UnmovableResource() { resource.close(); }
UnmovableResource& operator=(UnmovableResource&&) = delete;
};